using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.Sockets;
using System.Threading;
using Trace=InfiniTec.Diagnostics.Trace;

namespace InfiniTec.Threading
{
	public class AsyncOperation
	{
		public event AsyncCompletedEventHandler Completed;

		/// <summary>
		/// Occurs when the progress status of the operation changes.
		/// </summary>
		public event ProgressStatusChangedEventHandler ProgressChanged;

		[ThreadStatic] private static AsyncOperation _Current;

		private static Trace _Trace = new Trace("Infinitec.Threading");
		private static readonly ResourceReservationCollection _ResourceReservations = new ResourceReservationCollection();

		private readonly bool _AllowMultipleInvocations;
		private readonly AsyncOperation _BaseOperation;

		private readonly List<OperationInfo> _ChildOperations = new List<OperationInfo>();
		private readonly object _ChildOperationsLock = new object();
		private readonly Guid _Id = Guid.NewGuid();
		private readonly string _Name;
		private readonly IEnumerable<OperationAction> _Operation;
		private readonly Dictionary<Type, OperationActionHandler> _OperationActionHandler = new Dictionary<Type, OperationActionHandler>();
		private readonly IEnumerator<OperationAction> _OperationsEnumerator;
		private readonly SynchronizationContext _SynchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext();
		private readonly object _TimeoutLock = new object();
		private readonly object _UserState;
		private Exception _Error;
		private int _MaximumParallelChildOperations = int.MaxValue;
		private readonly SpinWaitLock _AdvanceLock = new SpinWaitLock();

		private ManualResetEvent _OperationCompletedSignal;
		private OperationStatus _OperationStatus;
		private AsyncOperation _Parent;
		private bool _RunningSynchronously;
		private DateTime _StartTime;
		private TimeSpan? _Timeout;
		private DispatcherCookie _TimeoutHandle;
		private AsyncOperationTracker _Tracker;
		private DispatcherCookie _WaitOperationCookie;
		private IResourceConstraint _ResourceConstraint;

		/// <summary>
		/// Initializes a new instance of the <see cref="AsyncOperation"/> class.
		/// </summary>
		/// <param name="name">The name of the operation</param>
		/// <param name="operation">The operation to execute.</param>
		/// <param name="tracker">The tracker in which this operation is enlisted.</param>
		/// <param name="allowMultipleInvocations">if set to <c>true</c> the operation can be called multiple times.</param>
		/// <param name="userState">A user defined stated which can be used to identify this instance.</param>
		public AsyncOperation(string name, IEnumerable<OperationAction> operation, AsyncOperationTracker tracker, bool allowMultipleInvocations, object userState) :
			this(operation, userState)
		{
			if (tracker == null) throw new ArgumentNullException("tracker");
			if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name");

			_Name = name;
			_Tracker = tracker;
			_AllowMultipleInvocations = allowMultipleInvocations;
		}

		public IResourceConstraint ResourceConstraint
		{
			get { return _ResourceConstraint; } 
			set
			{
				if (_OperationStatus != OperationStatus.NotStarted)
				{
					throw new InvalidOperationException("The ResourceConstraint can only be set before the operation has started.");
				}
				_ResourceConstraint = value;
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="AsyncOperation"/> 
		/// </summary>
		/// <param name="operation">The operation to execute</param>
		/// <param name="userState">A user defined stated which can be used to identify this instance.</param>
		public AsyncOperation(IEnumerable<OperationAction> operation, object userState)
		{
			if (operation == null) throw new ArgumentNullException("operation");

			_UserState = userState;
			_Operation = operation;
			_OperationsEnumerator = _Operation.GetEnumerator();

			InitializeOperationActionHandlers();
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="AsyncOperation"/> class and provides access to the base operation
		/// provided by <see cref="baseOperation"/> via the <see cref="ExecuteBaseOperation"/> action.
		/// </summary>
		/// <param name="operation">The operation to execute.</param>
		/// <param name="baseOperation">The base operation to execute when a <see cref="ExecuteBaseOperation"/> is returned by the <see cref="operation"/>.</param>
		/// <param name="userState">A user defined stated which can be used to identify this instance.</param>
		public AsyncOperation(IEnumerable<OperationAction> operation, AsyncOperation baseOperation, object userState)
			: this(operation, userState)
		{
			if (baseOperation == null) throw new ArgumentNullException("baseOperation");

			_BaseOperation = baseOperation;
			_Tracker = _BaseOperation._Tracker;
			_Name = _BaseOperation.Name;
			_AllowMultipleInvocations = _BaseOperation._AllowMultipleInvocations;

			_BaseOperation._Tracker = null;
		}

		/// <summary>
		/// Gets or sets the timeout for this asyncronous method. 
		/// </summary>
		/// <value>The timeout.</value>
		/// <remarks>This property can be modified during the execution of the operation.</remarks>
		public TimeSpan? Timeout
		{
			get
			{
				return _Timeout;
			}
			set
			{
				_Timeout = value;

				if (_TimeoutHandle != null)
				{
					lock (_TimeoutLock)
					{
						if (_TimeoutHandle != null)
						{
							UnregisterTimeoutCallback();
							if (value != null)
							{
								RegisterTimeoutCallback();
							}
						}
					}
				}
			}
		}

		/// <summary
		/// Gets the asynchronous operation that is currently executed on the active thread.
		/// </summary>
		/// <value>The current.</value>
		public static AsyncOperation Current { get { return _Current; } }


		/// <summary>
		/// Gets the synchronization context which is used when invoking events on this instance.
		/// </summary>
		/// <value>The synchronization context.</value>
		public SynchronizationContext SynchronizationContext { get { return _SynchronizationContext; } }

		/// <summary>
		/// Gets the name of the operation.
		/// </summary>
		/// <value>The name.</value>
		public string Name { get { return _Name; } }

		/// <summary>
		/// Gets the id of the operation.
		/// </summary>
		/// <value>The id.</value>
		public Guid Id { get { return _Id; } }

		internal Dictionary<Type, OperationActionHandler> OperationActionHandler { get { return _OperationActionHandler; } }

		/// <summary>
		/// Gets or sets the maximum number of child operations which are executed in paralell when a
		/// <see cref="ParallelOperation"/> action is returned from the operation being executed.
		/// </summary>
		/// <value>The maximum parallel child operations.</value>
		public int MaximumParallelChildOperations { get { return _MaximumParallelChildOperations; } set { _MaximumParallelChildOperations = value; } }


		/// <summary>
		/// Gets the error that was returned from this operation.
		/// </summary>
		/// <value>The error.</value>
		public Exception Error { get { return _Error; } }


		/// <summary>
		/// Gets a <see cref="WaitHandle"/> which can be used to block a thread until this operation is completed.
		/// </summary>
		/// <value>The operation completed signal.</value>
		public WaitHandle OperationCompletedSignal
		{
			get
			{
				if (_OperationCompletedSignal == null) _OperationCompletedSignal = new ManualResetEvent(false);
				return _OperationCompletedSignal;
			}
		}

		/// <summary>
		/// Gets the operation status.
		/// </summary>
		/// <value>The operation status.</value>
		public OperationStatus OperationStatus { get { return _OperationStatus; } }

		internal static Trace Trace { get { return _Trace; } set { _Trace = value; } }

		internal AsyncOperation ParentOperation { get { return _Parent; } }


		/// <summary>
		/// Defines an event handler used in event based asynchronous scenarios (like <see cref="Socket.SendAsync"/>).
		/// </summary>
		/// <param name="sender">The source of the event</param>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		/// <remarks>Use from inside the async operation via <c>AsyncOperation.Current.EventHandler</c></remarks>
		public void EventHandler(object sender, EventArgs e)
		{
			if (_OperationStatus == OperationStatus.Running)
			{
				Advance();
			}
		}

		private void InitializeOperationActionHandlers()
		{
			_OperationActionHandler.Add(typeof (WaitForHandleOperation), WaitOperationActionHandler);
			_OperationActionHandler.Add(typeof (OperationProgress), OperationProgressHandler);
			_OperationActionHandler.Add(typeof (NestedOperation), NestedOperationHandler);
			_OperationActionHandler.Add(typeof (RethrowOperation), RethrowOperationHandler);
			_OperationActionHandler.Add(typeof (ParallelOperation), ParallelOperationHandler);
			_OperationActionHandler.Add(typeof (ExecuteBaseOperation), ExecuteBaseOperationHandler);
			_OperationActionHandler.Add(typeof (WaitAction), ExecuteWaitOperationHandler);
			_OperationActionHandler.Add(typeof (WaitForEventOperation), ExecuteWaitForEventOperationHandler);
			_OperationActionHandler.Add(typeof(WaitForAsyncResultOperation), ExecuteWaitForAsyncResultOperation);
		}

		private AdvanceAction ExecuteWaitForAsyncResultOperation(OperationAction action)
		{
			var op = (WaitForAsyncResultOperation) action;

			if (!EventBasedAsyncResultAttribute.IsEventBasedAsyncResult(op.AsyncResult))
			{
				RegisterWaithandleCallback(op.AsyncResult.AsyncWaitHandle);
			}
			return AdvanceAction.ContinueAsynchronous;
		}

		private static AdvanceAction ExecuteWaitForEventOperationHandler(OperationAction action)
		{
			return AdvanceAction.ContinueAsynchronous;
		}

		private void RegisterTimeoutCallback()
		{
			if (_Timeout != null)
			{
				_TimeoutHandle = Dispatcher.RegisterCallback(_StartTime + _Timeout.Value, Timeout_Callback);
			}
		}

		private void Timeout_Callback(object state)
		{
			lock (_TimeoutLock)
			{
				_TimeoutHandle = null;

				_OperationStatus = OperationStatus.Timedout;
			}
			Advance(true);
		}

		private AdvanceAction ExecuteWaitOperationHandler(OperationAction action)
		{
			var waitAction = (WaitAction) action;

			lock (_TimeoutLock)
			{
				_WaitOperationCookie = Dispatcher.RegisterCallback(waitAction.Date, delegate { Advance(); });
			}

			return AdvanceAction.ContinueAsynchronous;
		}

		private AdvanceAction ExecuteBaseOperationHandler(OperationAction action)
		{
			if (_BaseOperation == null)
			{
				throw new InvalidOperationException("The base operation is not set.");
			}

			_Trace.Verbose("Executing base operation {0}", _BaseOperation.Name);
			AddChildOperation(_BaseOperation, new List<Type>());

			return AdvanceAction.ContinueAsynchronous;
		}

		internal static void StartAsync(AsyncOperation operation)
		{
			ThreadPool.QueueUserWorkItem(delegate { operation.Advance(); });
		}

		private AdvanceAction ParallelOperationHandler(OperationAction obj)
		{
			var operation = (ParallelOperation) obj;

			_Trace.Verbose(0, "Executing {0} child operations in paralell ({0} at a time)", operation.Operations.Count,
			               Math.Min(MaximumParallelChildOperations, operation.MaximumParallelOperations));
			_MaximumParallelChildOperations = operation.MaximumParallelOperations;

			var children = new List<string>();

			foreach (NestedOperation nestedOperation in operation.Operations)
			{
				children.Add(nestedOperation.Operation.Name);
				AddChildOperation(nestedOperation.Operation, nestedOperation.ExceptionsToHandle);
			}
			_Trace.Data(TraceEventType.Verbose, 0, children.ToArray());

			RunChildOperations();

			return AdvanceAction.ContinueAsynchronous;
		}

		private AdvanceAction RethrowOperationHandler(OperationAction obj)
		{
			var operation = (RethrowOperation) obj;

			_Trace.Verbose(0, "Rethrowing {0}: {1}", operation.Exception.GetType().FullName, operation.Exception);

			HandleException(operation.Exception);

			return AdvanceAction.ContinueAsynchronous;
		}

		private void HandleException(Exception exception)
		{
			_Error = exception;
			if (_OperationStatus != OperationStatus.Timedout)
			{
				_OperationStatus = OperationStatus.Failed;
			}
		}

		private AdvanceAction NestedOperationHandler(OperationAction obj)
		{
			var operation = (NestedOperation) obj;

			AddChildOperation(operation.Operation, operation.ExceptionsToHandle);

			return AdvanceAction.ContinueAsynchronous;
		}

		private void AddChildOperation(AsyncOperation operation, List<Type> exceptionsToHandle)
		{
			operation._Parent = this;

			lock (_ChildOperationsLock)
			{
				_ChildOperations.Add(new OperationInfo(operation, exceptionsToHandle));
			}

			RunChildOperations();
		}

		private void RunChildOperations()
		{
			int index = 0;

			lock (_ChildOperationsLock)
			{
				while (index < _ChildOperations.Count &&
				       _ChildOperations[index].Operation.OperationStatus != OperationStatus.NotStarted)
				{
					index++;
				}

				while (index < Math.Min(_ChildOperations.Count, _MaximumParallelChildOperations))
				{
					AsyncOperation operation = _ChildOperations[index].Operation;

					//_Trace.Transfer(0, operation.Id, "Executing child operation {0}", operation.Name);

					operation.Run();
					index++;
				}
			}
		}


		private AdvanceAction OperationProgressHandler(OperationAction obj)
		{
			var action = (OperationProgress) obj;

			if (OnProgressChanged(new ProgressStatusChangedEventArgs(action.Status, action.ProgressPercentage, _UserState)))
			{
				return AdvanceAction.ContinueSynchronous; 
			}

			if (_Parent != null)
			{
				_Parent.OperationProgressHandler(obj);
			}

			return AdvanceAction.ContinueSynchronous;
		}

		private bool OnProgressChanged(ProgressStatusChangedEventArgs e)
		{
			if (_RunningSynchronously) return false;

			ProgressStatusChangedEventHandler handler = ProgressChanged;

			if (handler != null)
			{
				_SynchronizationContext.Send(delegate { handler(this, e); }, null);
				return true;
			}
			return false;
			
		}

		private AdvanceAction WaitOperationActionHandler(OperationAction action)
		{
			var operation = (WaitForHandleOperation) action;

			RegisterWaithandleCallback(operation.WaitHandle);

			return AdvanceAction.ContinueAsynchronous;
		}

		private void RegisterWaithandleCallback(WaitHandle handle)
		{
			ThreadPool.RegisterWaitForSingleObject(handle, WaitOperationHandle_Callback, null, System.Threading.Timeout.Infinite, true);
		}

		private void WaitOperationHandle_Callback(object state, bool timedOut)
		{
			if (timedOut) return;

			Advance();
		}

		internal void Advance()
		{
			Advance(false);
		}

		private void Advance(bool timedOut)
		{
			_AdvanceLock.Enter();

			try
			{
				if (!timedOut && _OperationStatus == OperationStatus.Timedout)
				{
					return;
				}

				bool finished;
				do
				{
					try
					{
						if (_OperationStatus == OperationStatus.ExecutionPending)
						{
							_OperationStatus = OperationStatus.Running;
						}

						if (timedOut)
						{
							if (_WaitOperationCookie != null)
							{
								Dispatcher.CancelCallback(_WaitOperationCookie);
							}
							HandleException(new TimeoutException());
							break;
						}


						//Trace.ActivityId = Id;
						Trace.Verbose("Executing next step of the operation");
						_Current = this;
						finished = !_OperationsEnumerator.MoveNext();
						//Trace.ActivityId = Id;
					}
					catch (Exception ex)
					{
						//Trace.ActivityId = Id;
						_Trace.Error("An exception of type {0} occured during the execution of the operation step: {1}",
						             ex.GetType().FullName, ex);
						HandleException(ex);

						break;
					}
					if (!finished)
					{
						Type operationActionType = _OperationsEnumerator.Current.GetType();

						_Trace.Verbose("Executing operation action of type {0}", operationActionType.FullName);
						OperationActionHandler handler = _OperationActionHandler[operationActionType];

						if (handler(_OperationsEnumerator.Current) == AdvanceAction.ContinueAsynchronous) return;

						if (_OperationStatus != OperationStatus.Running)
						{
							finished = true;
						}
					}
					else _OperationStatus = OperationStatus.Completed;
				} while (!finished);

				UnwindOperation();

				if (_Error != null)
				{
					PropagateExceptionToParent();
				}
			}
			finally
			{
				_AdvanceLock.Exit();
			}
		}

		private void UnwindOperation()
		{
			AbortChildOperations();

			var disposable = _Operation as IDisposable;
			if (disposable != null)
			{
				_Trace.Verbose("Completing the execution of the operation");
				disposable.Dispose();
			}

			CompleteOperation();
		}

		private void PropagateExceptionToParent()
		{
			if (_Parent != null)
			{
				//Trace.Transfer(0, _Parent.Id, "Propagating the current exception to the parent operation");
				_Parent.HandleExceptionFromChildOperation(Error, this);
			}
		}

		private void CompleteOperation()
		{
			if (_TimeoutHandle != null)
			{
				lock (_TimeoutLock)
				{
					if (_TimeoutHandle != null)
					{
						UnregisterTimeoutCallback();
						_TimeoutHandle = null;
					}
				}
			}

			_ResourceReservations.ReleaseOperation(this);

			if (_Tracker != null) _Tracker.Unregister(Name, _UserState, this);

			// Only remove this operation from the parent operation if the operation completed without exception
			// The PropagateExceptionToParent takes care of this
			if (_Parent != null && _Error == null)
			{
				//_Trace.Transfer(0, _Parent.Id, "Removing operation from the parents list of child operations");
				_Parent.RemoveChildOperation(this);
			}

			if (_OperationCompletedSignal != null)
			{
				_Trace.Verbose("Signaling waiting processes that this operation is no longer running.");
				_OperationCompletedSignal.Set();
			}

			switch (OperationStatus)
			{
				case OperationStatus.Completed:
					OnCompleted(new AsyncCompletedEventArgs(null, false, _UserState));
					break;
				case OperationStatus.Aborted:
					OnCompleted(new AsyncCompletedEventArgs(null, true, _UserState));
					break;
				case OperationStatus.Failed:
					OnCompleted(new AsyncCompletedEventArgs(_Error, false, _UserState));
					break;
				case OperationStatus.Timedout:
					OnCompleted(new AsyncCompletedEventArgs(_Error, false, _UserState));
					break;
			}
		}

		private void UnregisterTimeoutCallback()
		{
			DispatcherCookie dispatcherCookie = Interlocked.Exchange(ref _TimeoutHandle, null);
			
			if (dispatcherCookie == null)
			{
				return;
			}
			Dispatcher.CancelCallback(dispatcherCookie);
		}

		/// <summary>
		/// Synchronously starts the execution of this operation.
		/// </summary>
		public void RunSynchronously()
		{
			_RunningSynchronously = true;

			Run();

			if (_OperationStatus == OperationStatus.Running || _OperationStatus == OperationStatus.ExecutionPending)
			{
				OperationCompletedSignal.WaitOne();
			}

			if (Error == null)
			{
				return;
			}
			if (ExceptionHelper.PreserveExceptionStackTrace(Error))
			{
				throw Error;
			}

			throw new AsyncOperationFailedException(
				string.Format("The operation has failed. See the inner exception for more details. Error: {0}", Error.Message), Error);
		}


		private void OnCompleted(AsyncCompletedEventArgs e)
		{
			if (_RunningSynchronously) return;

			if (Completed != null) _SynchronizationContext.Send(delegate { Completed(this, e); }, null);
		}

		/// <summary>
		/// Asynchronously starts the execution of this operation.
		/// </summary>
		public void Run()
		{
			if (_Tracker != null)
			{
				//Trace.ActivityId = Id;
				_Tracker.Register(_Name, _UserState, this, _AllowMultipleInvocations);
			}
			_OperationStatus = OperationStatus.ExecutionPending;

			_StartTime = DateTime.Now;
			RegisterTimeoutCallback();

			if (_RunningSynchronously)
			{
				_ResourceReservations.RegisterOperation(this);
			}
			else
			{
				ThreadPool.QueueUserWorkItem(delegate { _ResourceReservations.RegisterOperation(this); });
			}
		}

		private void RemoveChildOperation(AsyncOperation childOperation)
		{
			bool advance;

			if (childOperation == null) throw new ArgumentNullException("childOperation");

			lock (_ChildOperationsLock)
			{
				_ChildOperations.RemoveAll(info => info.Operation == childOperation);

				advance = _ChildOperations.Count == 0;
				if (!advance) RunChildOperations();
			}

			if (advance && OperationStatus == OperationStatus.Running) Advance();
		}

		private void HandleExceptionFromChildOperation(Exception ex, AsyncOperation childOperation)
		{
			OperationInfo info;

			lock (_ChildOperationsLock)
			{
				info = _ChildOperations.Find(oi => oi.Operation == childOperation);

				if (!info.HandlesException(ex))
				{
					// Fail the curren operation
					HandleException(ex);

					RemoveChildOperation(childOperation);

					AbortChildOperations();

					CompleteOperation();

					// Bubble the exception to the parent operation
					if (_Parent != null) _Parent.HandleExceptionFromChildOperation(ex, this);
				}
				else RemoveChildOperation(childOperation);
			}
		}

		private void AbortChildOperations()
		{
			AsyncOperation child;

			if (_ChildOperations.Count == 0) return;

			lock (_ChildOperationsLock)
			{
				for (int i = _ChildOperations.Count - 1; i >= 0; i--)
				{
					child = _ChildOperations[i].Operation;
					_ChildOperations.RemoveAt(i);

					child.Cancel();
				}
			}
		}

		/// <summary>
		/// Gracefully aborts the execution of this method.
		/// </summary>
		public void Cancel()
		{
			if (_OperationStatus == OperationStatus.Running) _OperationStatus = OperationStatus.Aborted;

			UnwindOperation();
		}
	}
}